<!DOCTYPE html>
<html lang="zh-cn">
  <head>
    <meta charset="UTF-8">
    <title>Sucha's Blog - Archive for April, 2021</title>
    <meta name="generator" content="MarkdownProjectCompositor.lua">
    <meta name="author" content="Sucha">
    <meta name="keywords" content="suchang, programming, Linux, Lua">
    <meta name="description" content="Sucha's blog">
    <link rel="shortcut icon" href="../images/ico.png">
    <link rel="stylesheet" type="text/css" href="../styles/blog.css">
    <link rel="stylesheet" type="text/css" href="../styles/prism.min.css">
    <style id="site_theme"></style>
  </head>
  <body>
    <div id="body">
      <div id="text">
	   <!-- Page published by cmark-gfm begins here --><h1>Sucha's Blog ~ Archive for April, 2021</h1>
<p><a id="p1"></a></p>
<div class="date">21年4月11日 周日 21:23</div>
<h2>iOS 的异步刷新</h2>
<p>技术细节大概描述一下就好，因为网上能搜到的内容蛮多的，这里主要说一下工作中涉及到的方面。</p>
<p>项目之前因为要 0.5 秒刷新一下部分界面，主要是数据更新，涉及到文字、以及用于标记数值程度的背景色。之前的负责人估计第一版用的是 UILabel，或者就是直接用了脸书的 Texture，就为了更新这一小块内容，居然用了 Texture，不可思议。</p>
<p>整个页面除了这里，应该也有其他的原因，反正有点卡，在 i7 32G 的 14.4 模拟器上，CPU 占用率大概 20%，其他部分算法的原因，在这个范围 -+4% 浮动。</p>
<p>我用 instrument 的 time profile 看了一下，Texture 的 layout 部分占了大头，因为需要更新标记数值程度的背景色，因此 cell node 整个 layout 更新，因为又是 RxSwift drive 了整个 table view items，所以实际上是整个 table view node 不停 layout 更新了。</p>
<p>即便 Texture 用的是 Yoga，但是就这两个变更点的更新，用得着 layout 整个 table view node 吗。</p>
<p>于是上周搜索了一下异步更新的方案，基本上就是 CPU 方案 vs GPU 方案，CPU 方案其实就是 Core Graphics，UIKit 其实就是 CPU 的，而 Core Animation 是 GPU 的，比如 CATextLayer、CAShapeLayer、CAGradientLayer，而且 CALayer 有 drawsAsynchronously 这个属性，设置为 true 后，就是异步线程刷新，文档建议自己试试看。</p>
<p>那么方案差不多出来了，选用 GPU 的方案，固定 frame，数据 drive 比如背景色，只更新相关 cell 背景 layer 的 frame，打开异步线程刷新，这样 layout 的负载会低一些。</p>
<p>另外实操后发现，CATextLayer 其实能力是比较有限的，跟 UILabel 有很大不同</p>
<ul>
<li>没有自动多行的概念，需要设置 warp 属性，不过当前项目也不需要多行就是</li>
<li>绘图 origin 固定是 (0,0)，意味着没有默认的 vertical 居中的效果</li>
<li>font 属性是 CTFont，这个需要 CTFontCreateWithName 从 UIFont 拿 font name 再创建</li>
<li>font size 还是单独的，我是从 UIFont 的 pointSize 设置过去了</li>
<li>水平居左、中、右跟 UILabel 是类似的了</li>
</ul>
<p>因为需要配置 CTFont，以及自己计算行高后做 vertical center，于是再封装了一层普通 CALayer，将 UIFont 暴露出来，以及默认 vertical center，这个项目暂时不需要 vertical 的其他配置，就先不做这个部分的可选项了。</p>
<p>标记数值变化的背景色更新，用的就是普通的 CALayer，因为 frame 设置后，本来就有 tween 补间动画，只是配置颜色输入时不是 UIColor，而是 CGColor 罢了。</p>
<p>类似 tableview 的 UI 框架部分，用了之前介绍的 StackViewLayout，cell view 可以是任何的普通 view，不一定是需要同一种 view，只要支持 sizeToFits 就可以自动配置行高了。</p>
<p>配置了 rx 数据接口后，塞数据进去也很方便，上述的修改，在 i7 32G 的 14.4 模拟器上，CPU 降了大概 8%，在 6 plus 上降了有 10% 多。</p>
<p>是属于比较明显的改进了。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2021-04.html#p1">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<p><a id="p0"></a></p>
<div class="date">21年4月02日 周五 20:55</div>
<h2>iOS 14 NavigationBar</h2>
<p>工程在我进公司之前就遇到了这样的问题，导航栏左右 bar item 在 iOS 13 是好好的，但在 iOS 14 上面，从一级页面 push 进入到二级页面，进入到三级页面，或者从二级页面 pop 到一级页面，都会往中间跳动缩进一下，再回归到正常位置。</p>
<p>这种跳动感觉很突兀，很别扭，但是很难定位原因。新建 demo 工程自己试了一下，iOS 14 下是没问题的，我们有用一些第三方库，其中 QMUI 是涉及比较广的 UI 库，但用 QMUI 官方的 demo 试了一下，iOS 14 下也是好好的。</p>
<p>因为用的 QMUI 版本有点旧了，是 4.1.3 在 2020 年 5 月 release 的，于是早上更新到了 4.2.3，但还是没用，问题依旧。</p>
<p>模拟器断点看了 UI 层级，我们是用 custom view 放到 bar button item 里面的，custom view 的 superview 是一个  _UITAMICView 之类的，它的 superview 是 _UIButtonStackView 大概这个名字，它的 superview 是 _UINavigationBarContentView 这样的，最后才是 NavigationBar。</p>
<p>问题是除了 custom view，其他都是系统的 view，一开始观察，push、pop 的时候，其实是 _UIButtonStackView 和 _UITAMICView 都的 frame 有变化，一开始 frame x 是 0，稳定后 stack frame x 变为 -8，tamic frame x 变为 8，然后我们的 custom view frame x 是 0，刚好跟 navgation bar 对齐。</p>
<p>于是我就用 rxswift 的 observ 监听 _UITAMICView 的 frame，并做修改，发现不管用，因为总是断断续续在忙这个项目（对，其他插入的任务更紧急），第二天才发现无法直接监听的原因，是我自己对 navigation bar 了解不够所致。</p>
<p>navigation bar 的显示，开放给用户的接口是配置 navigation item，其实是个堆栈，最顶层的就是当前的显示配置。而 navigation item 可以配置左右 navigation bar item，还可以配置多个，其中左边是从左到右序列号增大排列，右边是从右到左序列号增大排列。</p>
<p>其 UI 层次规则，是每一个 navigation bar item 对应一组 _UIButtonStackView 和 _UITAMICView。因为 navigation item 左右两边 bar item 是可以动态配置的，因此这一组 view 层级也是动态显示的。</p>
<p>即便一开始就配置好了顶层 navigation item，但是 push 了新的 view controller 进来，顶层的 navigation item 更新后，新的 bar item custom view 会使用新建里的一组 stack view 和 tamic view。pop 后也类似，顶层 navigation item 更新，刚成为堆栈顶层 navigation item 左右 bar item 的 custom view 才建立 stack view 和 tamic view。</p>
<p>所以看到的缩进、跳动然后回归正常的左右 bar item custom view 动画，就是因为这种动态创建容器 view，然后 layout 造成的。</p>
<p>这就解释了之前为什么监听这两个 view 不成功，因为 pop 后才新建立的，之前建立的 observ 被 dispose 了。</p>
<p>继续观察，发现在 push、pop 的过程中，custom view frame 其实也会被设置的，一开始被设置为宽度更小的 frame，最后才设置为正常的 frame，原因我是搞不懂，但是 custom view 一直没有被释放，可以作为一个监听的基点。</p>
<p>于是做了改进，监听 custom view 的 frame，同时监听 .initial 和 .new，根据其在左右 bar items 的位置，配置对于 navigation bar 的偏移，这样就对了。</p>
<p>于是整套方案定下来了，最后结果也还说的过去，大概是继承 UINavigationBar，在建立 navigation controller 的时候，有初始化方法是可以传递 navigation bar class 进去的。</p>
<p>然后在 pushItem，以及 layoutSubviews 的时候，检查顶层 navigation item 的左右 bar items custom view，并将其左边前一个，或右边前一个的 view 记录下来。我是建立了一个 weak to weak 的 NSHashMap 来做记录的，然后建立对 custom view frame 的监听。</p>
<p>每次 frame 有更新，就检查 custom view 的 super view，其实就是 _UITAMICView，同时根据 custom view，找到排在它前面的另一个 custom view 的 _UITAMICView，根据前一个 tamic view 在 navigation bar 的位置，加上 8，作为当前 custom view _UITAMICView frame 的 x。</p>
<p>有点绕口了，但思路是没问题的，UI 表现上也说得过去。</p>
<p>说一下不好定位根本问题的原因，一方面不想做得更复杂了，系统类没有 hook，不好定位其修改点，且另外一方面，custom view frame 的修改，是在 QMUI hook 掉所有 view 的 layoutSubview 里面做的，这个到底是谁修改的，说不通了。</p>
<p>很无奈，不得不找一种半调子的解决办法。</p>
<div class="category"><a href="CategoryProgramming.html">CategoryProgramming</a> / <a href="2021-04.html#p0">Permalink</a> / <a href="https://github.com/lalawue/homepage/discussions/categories/blog" target="_blank">Discussion</a></div>
<!-- Page published by cmark-gfm ends here -->
  <div id="foot">2004-<script>var d = new
	Date();document.write(d.getFullYear())</script> &copy;
	Sucha. Powered by MarkdownProjectCompositor.
  </div>
  </div><!-- text -->
  <div id="sidebar">
  </div><!-- sidebar -->
  <script src="../js/prism.min.js" async="async"></script>
  <script src="../js/blog_sidebar.js"></script>
  </div> <!-- body -->
</body>
</html>